<template>
  <div ref="root" class="waterfall">
    <div class="slot-box">
      <slot />
    </div>
  </div>
</template>

<script>
export default {
  name: 'Waterfall',
  props: {
    // 列数
    cols: {
      type: Number,
      default: 2
    },
    // 数据列表
    dataList: {
      type: Array,
      default: () => []
    },
    // 每一列的宽度
    width: Number,

    // 距离底部多远进行上拉刷新
    reachBottomDistance: {
      type: Number,
      default: 300
    },

    // 滚动节流间隔事件，ms
    interval: {
      type: Number,
      default: 100
    }
  },

  data() {
    return {
      root: null, // 组件根元素
      colNodes: [], // 每一列的元素
      canLoad: true, // 是否可以加载更多
      renderIndex: 0 // 已渲染的元素索引
      // isRendering: false
    }
  },
  watch: {
    // 数据列数发生改变即重新渲染
    cols(nVal, oVal) {
      if (nVal === oVal) {
        return
      }
      this.init()
    },

    // 数据发生改变，则删除所有列重新添加列中元素(调用clearItem())
    dataList(nVal, oVal) {
      if (!nVal.length || nVal.length < oVal.length) {
        this.clearItem()
      }
      this.$nextTick(() => {
        setTimeout(() => {
          this.createColItem()
        }, 0)
      })
    }
  },
  mounted() {
    this.init()
    this.scrollEvent()
  },
  methods: {
    // 开始渲染(调用clearColumn(), createColumn(), createColItem())
    init() {
      // 获取根元素
      this.root = this.$refs.root
      // 初始化数据 调用clearColumn()
      this.canLoad = true
      this.renderIndex = 0
      this.clearColumn()
      // 创建列 （调用createColumn()）
      this.createColumn()

      // 创建列中的元素 （渲染完成后调用：this.$nextTick()）
      this.$nextTick(() => {
        setTimeout(() => {
          this.createColItem()
        }, 0)
      })
    },

    // 清除之前定义的列
    clearColumn() {
      this.colNodes.forEach((item) => {
        item.remove()
      })
      this.colNodes = []
    },

    // 清除列中的元素内容（数据变化时调用）
    clearItem() {
      if (!this.colNodes || !this.colNodes.length) {
        return
      }
      // 遍历每列中的元素，改变其内容
      this.colNodes.forEach((item) => {
        // eslint-disable-next-line no-param-reassign
        item.innerHTML = ''
      })
      // 初始化数据
      this.renderIndex = 0
      this.canLoad = true
    },

    // 创建列(默认2列，this.cols)
    createColumn() {
      const colNodes = []
      // 判断有几列 (默认2列，width: 50%)
      for (let index = 0; index < this.cols; index += 1) {
        // 创建列,class=" waterfall__cols "
        const node = document.createElement('div')
        node.className = 'waterfall-cols'
        // 若给定宽度，则按照给定宽度设置列宽度
        if (this.width) {
          node.style.width = `${this.width}px`
        } else {
        // 无宽度，以百分比给宽（四舍五入）
          const width = Math.round((1 / this.cols) * 100)
          node.style.width = `${width}%`
        }
        // 判断是否是根元素
        if (!this.root) {
          this.root = this.$refs.root
        }
        // 将列添加进数组中
        // eslint-disable-next-line no-unused-expressions
        this.root && this.root.appendChild(node)
        colNodes.push(node)
      }
      this.colNodes = colNodes
    },
    // 创建列中的元素(调用getShortColumns, appendChild, loadMores方法)
    // 通过列高度判断元素加入哪列([1,2,5,3,4,6,2,3] ===> [1,5,6][2,3,4,2,3])
    async createColItem() {
      // 判断状态以及是否获取到列表数据
      if (this.isRendering || !this.$slots.default || !this.$slots.default.length) {
        return
      }
      this.isRendering = true
      // nodes 获取子元素中的具体内容(从renderIndex开始添加：0)
      const nodes = this.$slots.default.splice(this.renderIndex)
      for (let index = 0; index < nodes.length; index += 1) {
        // 获取最短的列
        const shortCol = this.getShortColumns()
        if (!shortCol) {
          return
        }
        // 将元素放入短列中(调用appendChild方法)
        await this.appendChild(shortCol, nodes[index].elm)
        this.renderIndex += 1
      }
      this.isRendering = false

      // 如果屏幕没有占满，则加载更多数据
      this.loadMores()
    },

    // 获取短列
    getShortColumns() {
      // 获取当前colNodes的长度
      const { length } = this.colNodes
      if (!length) {
        return
      }
      // 第一列
      let shortNode = this.colNodes[0]
      for (let index = 1; index < length; index += 1) {
        // 第二列及之后列的高度
        const height = this.colNodes[index].offsetHeight
        if (height < shortNode.offsetHeight) {
          shortNode = this.colNodes[index]
        }
      }

      return shortNode
    },

    // 添加元素(将子元素添加到父元素中)
    async appendChild(parent, node) {
      // 判断子元素是否存在及其类型
      if (!node || !(node instanceof HTMLElement)) {
        return
      }
      // 获取列表中的所有图片
      const imgs = node.getElementsByTagName('img')
      // 若不存在图片，直接放入短列中
      if (!imgs || !imgs.length) {
        parent.appendChild(node)
        return Promise.resolve()
      }
      // 若存在图片，获取图片的宽度，并给图片占位，然后放入短列中
      for (let index = 0; index < imgs.length; index += 1) {
        const img = imgs[index]
        if (!img) {
          return
        }
      }
      parent.appendChild(node)
      return Promise.resolve()
    },

    // 添加更多,使用canLoad判断: true
    loadMores() {
      // 获取窗口高度及根元素底部位置
      const height = window.innerHeight
      const { bottom } = this.root.getBoundingClientRect()
      // 判断底部位置是否小于窗口高度与刷新距离reachBottomDistance之和
      if (bottom <= height + this.reachBottomDistance && this.canLoad) {
        this.canLoad = false
        this.$emit('loadmore')
      } else if (bottom > height + this.reachBottomDistance) {
        this.canLoad = true
      }
    },

    // 滚动触摸监听(scrollEvent —— loadMore ——> loadMores)
    scrollEvent() {
      window.onscroll = this.loadMore
      window.addEventListener('touchmove', this.loadMore, false)
    },
    // 节流 (间隔：this.interval)
    loadMore() {
      this.$emit('scroll')
      if (this.timer) {
        return
      }
      this.timer = setTimeout(() => {
        clearTimeout(this.timer)
        this.timer = null
        this.loadMores()
      }, this.interval)
    }
  }
}
</script>

<style lang="less">
.waterfall {
  box-sizing: border-box;
  width: 100%;
  position: relative;
  overflow-y: scroll;
  padding: 0 17px;
}
.waterfall-cols {
  box-sizing: border-box;
  float: left;
  margin: 0;
  padding: 0;
  padding: 0 7px;
}
.slot-box {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}
</style>
